package com.wsl.HealthChallenge.controllers;

import java.util.ConcurrentModificationException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.jdo.JDOHelper;
import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

import com.google.appengine.api.labs.taskqueue.Queue;
import com.google.appengine.api.labs.taskqueue.QueueFactory;
import com.google.appengine.api.labs.taskqueue.TaskOptions;
import com.google.gdata.util.AuthenticationException;
import com.wsl.HealthChallenge.models.ContestEntry;
import com.wsl.HealthChallenge.models.ContestType;
import com.wsl.HealthChallenge.models.UserEntry;
import com.wsl.HealthChallenge.models.UserStats;
import com.wsl.HealthChallenge.utils.DataStorageUtils;
import com.wsl.HealthChallenge.utils.PMF;

/**
 * 
 * @author mark
 */
@Controller
public class UpdateScoreController {
    private static final String END_TIMESTAMP = "endTime";
    private static final String START_TIMESTAMP = "startTime";
    private static final String GOOGLE_HEALTH_TOKEN = "token";

    public static final Logger log = Logger.getLogger(UpdateScoreController.class.getName());
    private static final String CONTEST_TYPE = "contestType";

//    private static int added = 0;
//    private static int started = 0;
//    private static int finished = 0;
//    private static int finishedSuccess = 0;
//    private static int numtotalTries = 0;
//    private static Set<String> updatedUsers = new HashSet<String>();

    @RequestMapping("/task/updatescore")
    public ModelAndView updateScoreHandler(HttpServletRequest request, 
            HttpServletResponse response) throws Exception {
        PersistenceManager pm = PMF.get().getPersistenceManager();
        Query query = pm.newQuery(ContestEntry.class);
        List<ContestEntry> results = (List<ContestEntry>) query.execute();

        Queue queue = getUpdateScoreQueue();
        for (ContestEntry challenge : results) {
            queue.add(TaskOptions.Builder.url(
                    getUpdateChallengeUrl(challenge.getId()).toString()));
        }
        return null;
    }

    @RequestMapping("/task/updatescore/{challengeId}")
    public ModelAndView updateScoreHandler(@PathVariable String challengeId,
            HttpServletRequest request, HttpServletResponse response) throws Exception {
        ContestEntry challenge = DataStorageUtils.getContest(challengeId);
        if (challenge == null) {
            response.getWriter().println("Challenge does not exist.");
            return null;
        }

//        added = 0;
//        started = 0;
//        finished = 0;
//        finishedSuccess = 0;
//        numtotalTries = 0;
//        updatedUsers = new HashSet<String>();


        Queue queue = getUpdateScoreQueue();
        Map<String, UserStats> allUserStats = challenge.getUserStats();
        for (Entry<String, UserStats> entry : allUserStats.entrySet()) {
            addUpdateParticipantTask(challengeId, challenge,
                    entry.getKey(), entry.getValue(), queue);
//            added++;
        }
        return null;
    }

    public static Queue getUpdateScoreQueue() {
        return QueueFactory.getQueue("update-score");
    }

    /**
     * Adds a task that will update the given participant in the given challenge.
     * @param challengeId
     * @param challenge
     * @param queue
     * @param entry
     */
    public static void addUpdateParticipantTask(String challengeId, ContestEntry challenge,
            String userId, UserStats userStats, Queue queue) {
        if (userStats.getGoogleHealthSessionToken() == null) {
            return;
        }

        String startTimestamp = String.valueOf(challenge.getStartDate().getTime());
        String endTimestamp = String.valueOf(challenge.getEndDate().getTime());

        String updateParticipantUrl = getUpdateParticipantUrl(challengeId, userId);
        queue.add(TaskOptions.Builder.url(updateParticipantUrl)
                .param(GOOGLE_HEALTH_TOKEN, userStats.getGoogleHealthSessionToken())
                .param(CONTEST_TYPE, challenge.getType().stringRepresentation)
                .param(START_TIMESTAMP, startTimestamp)
                .param(END_TIMESTAMP, endTimestamp));
    }

    /**
     * 
     * @param challengeId
     * @param entry
     * @return
     */
    public static String getUpdateParticipantUrl(String challengeId, String userId) {
        StringBuilder builder = getUpdateChallengeUrl(challengeId);
        builder.append("/");
        builder.append(userId);
        return builder.toString();
    }

    public static StringBuilder getUpdateChallengeUrl(String challengeId) {
        StringBuilder builder = new StringBuilder("/task/updatescore/");
        builder.append(challengeId);
        return builder;
    }

    @RequestMapping("/task/updatescore/{challengeId}/{userId}")
    public ModelAndView updateScoreHandler(
            @PathVariable String challengeId,
            @PathVariable String userId,
            @RequestParam(GOOGLE_HEALTH_TOKEN) String googleHealthSessionToken,
            @RequestParam(START_TIMESTAMP) long startTimestamp,
            @RequestParam(END_TIMESTAMP) long endTimestamp,
            @RequestParam(value=CONTEST_TYPE, defaultValue="") String contestTypeString,
            HttpServletRequest request, HttpServletResponse response) throws Exception {
//        started++;
        Date startDate = new Date(startTimestamp);
        Date endDate = new Date(endTimestamp);

        ServletContext servletContext = request.getSession().getServletContext();
        GoogleHealthCalorieRequester requester = new GoogleHealthCalorieRequester(servletContext);

        ContestType contestType = ContestType.getFromStringRepresentation(contestTypeString);

        try {
            int scoreValue = requester.getTrackerValue(request, response,
                    googleHealthSessionToken, contestType.googleHealthTracker, startDate, endDate);

            // Store score value;
            boolean success = false;
            int numRetries = 0;
            while (!success && numRetries < 8) {
                try {
                    success = storeScoreValue2(challengeId, userId, scoreValue);
                } catch (ConcurrentModificationException e) {
                    Thread.sleep(3000);
                }
                numRetries++;
            }
        } catch(AuthenticationException e) {
            log.log(Level.SEVERE, "Update-AuthenticationException: ", e);
            forgetInvalidToken(challengeId, userId, googleHealthSessionToken);
        }

//        finished++;
//        if (success){
//            finishedSuccess++;
//            updatedUsers.add(userId);
//        }
//        numtotalTries += numRetries;
//
//        //log.info("numRetries:" + numRetries);
//        System.out.println("numRetries:" + numRetries +
//                ", added:" + added +
//                ", started:" + started +
//                ", finished:" + finished +
//                ", finishedSuccess:" + finishedSuccess +
//                ", updatedUsers:" + updatedUsers.size() +
//                ", numtotalTries:" + numtotalTries
//                );
        //if (numRetries > 1) {
        //    response.getWriter().println("numRetries:" + numRetries);
        //}
        return null;
    }

    /**
     * Called when we get an invalid token exception.
     * Checks whether the central token that is stored in the userentry is a different one and
     * if so starts using it for this challenge or in case that they are the same and therefore both
     * invalid forgets them both.
     * 
     * @param challengeId
     * @param userId
     * @param googleHealthSessionToken
     */
    private void forgetInvalidToken(String challengeId, String userId,
            String googleHealthSessionToken) {
        UserEntry user = DataStorageUtils.getUserEntry(userId);
        if (user == null) {
            return;
        }
        if (googleHealthSessionToken == null) {
            return;
        }

        log.log(Level.SEVERE, "user.getGoogleHealthSessionToken():" + user.getGoogleHealthSessionToken());
        log.log(Level.SEVERE, "googleHealthSessionToken:" + googleHealthSessionToken);

        boolean centralTokenIsInvalid = googleHealthSessionToken.equals(user.getGoogleHealthSessionToken());
        if (centralTokenIsInvalid) {
            // The token stored in user is invalid, removed it.
            user.setGoogleHealthSessionToken(null);
            JDOHelper.makeDirty(user, "googleHealthSessionToken");
            DataStorageUtils.insertOrUpdateUser(user);
            log.log(Level.SEVERE, "user.setGoogleHealthSessionToken(null)");
        }

        // The token stored in the challenge is invalid
        // -> Switch to the central token (which might be null).
        ContestEntry challenge = DataStorageUtils.getContest(challengeId);
        if (challenge == null) {
            return;
        }

        UserStats stats = challenge.getUserStats().get(userId);
        if (stats == null) {
            return;
        }
        if (user.getGoogleHealthSessionToken() == null){
            log.log(Level.SEVERE, "stats.setGoogleHealthSessionToken()=" + "null");
        } else {
            log.log(Level.SEVERE, "stats.setGoogleHealthSessionToken()=" + user.getGoogleHealthSessionToken());
        }

        stats.setGoogleHealthSessionToken(user.getGoogleHealthSessionToken());
        JDOHelper.makeDirty(stats, "googleHealthSessionToken");
        JDOHelper.makeDirty(challenge, "userStats");
        DataStorageUtils.insertOrUpdateContest(challenge);
    }

//    private boolean setTokenInChallenge(String challengeId, String userId, String token) {
//        PersistenceManager pm = PMF.get().getPersistenceManager();
//        try {
//            ContestEntry challenge = null;
//            pm.currentTransaction().begin();
//            try {
//                challenge = pm.getObjectById(ContestEntry.class, challengeId);
//                Map<String, UserStats> allUserStats = challenge.getUserStats();
//                UserStats userStats = allUserStats.get(userId);
//                if (userStats != null) {
//                    userStats.setGoogleHealthSessionToken(token);
//                    JDOHelper.makeDirty(userStats, "score");
//                }
//                JDOHelper.makeDirty(challenge, "userStats");
//            } catch (JDOObjectNotFoundException e) {
//            }
//            pm.currentTransaction().commit();
//        //} catch (Exception e) {
//        //    return false;
//        } finally {
//            if (pm.currentTransaction().isActive()) {
//                pm.currentTransaction().rollback();
//            }
//        }
//        return true;
//    }

    private boolean storeScoreValue2(String challengeId, String userId, int calories) {
        PersistenceManager pm = PMF.get().getPersistenceManager();
        try {
            ContestEntry challenge = null;
            pm.currentTransaction().begin();
            try {
                challenge = pm.getObjectById(ContestEntry.class, challengeId);
                Map<String, UserStats> allUserStats = challenge.getUserStats();
                UserStats userStats = allUserStats.get(userId);
                if (userStats != null) {
                    userStats.setScore(calories);
                    JDOHelper.makeDirty(userStats, "score");
                }
                JDOHelper.makeDirty(challenge, "userStats");
            } catch (JDOObjectNotFoundException e) {
            }
            pm.currentTransaction().commit();
        //} catch (Exception e) {
        //    return false;
        } finally {
            if (pm.currentTransaction().isActive()) {
                pm.currentTransaction().rollback();
            }
        }
        return true;
    }
}